package io.ebeaninternal.server.core;

import io.ebean.ValuePair;
import io.ebean.annotation.DocStoreMode;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.PreGetterCallback;
import io.ebean.event.BeanPersistController;
import io.ebean.event.BeanPersistListener;
import io.ebean.event.BeanPersistRequest;
import io.ebean.event.changelog.BeanChange;
import io.ebeaninternal.api.ConcurrencyMode;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.api.TransactionEvent;
import io.ebeaninternal.server.cache.CacheChangeSet;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanManager;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import io.ebeaninternal.server.deploy.id.ImportedId;
import io.ebeaninternal.server.persist.BatchControl;
import io.ebeaninternal.server.persist.BatchedSqlException;
import io.ebeaninternal.server.persist.PersistExecute;
import io.ebeaninternal.server.transaction.BeanPersistIdMap;
import io.ebeanservice.docstore.api.DocStoreUpdate;
import io.ebeanservice.docstore.api.DocStoreUpdateContext;
import io.ebeanservice.docstore.api.DocStoreUpdates;

import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * PersistRequest for insert update or delete of a bean.
 */
public final class PersistRequestBean<T> extends PersistRequest implements BeanPersistRequest<T>, DocStoreUpdate, PreGetterCallback {

  private final BeanManager<T> beanManager;

  private final BeanDescriptor<T> beanDescriptor;

  private final BeanPersistListener beanPersistListener;

  /**
   * For per post insert update delete control.
   */
  private final BeanPersistController controller;

  /**
   * The bean being persisted.
   */
  private final T bean;

  private final EntityBean entityBean;

  /**
   * The associated intercept.
   */
  private final EntityBeanIntercept intercept;

  /**
   * The parent bean for unidirectional save.
   */
  private final Object parentBean;

  private final boolean dirty;

  private final boolean publish;

  private DocStoreMode docStoreMode;

  private ConcurrencyMode concurrencyMode;

  /**
   * The unique id used for logging summary.
   */
  private Object idValue;

  /**
   * Hash value used to handle cascade delete both ways in a relationship.
   */
  private Integer beanHash;

  /**
   * Flag set if this is a stateless update.
   */
  private boolean statelessUpdate;

  private boolean notifyCache;

  private boolean deleteMissingChildren;

  /**
   * Flag used to detect when only many properties where updated via a cascade. Used to ensure
   * appropriate caches are updated in that case.
   */
  private boolean updatedManysOnly;

  /**
   * Many properties that were cascade saved (and hence might need caches updated later).
   */
  private List<BeanPropertyAssocMany<?>> updatedManys;

  /**
   * Need to get and store the updated properties because the persist listener is notified
   * later on a different thread and the bean has been reset at that point.
   */
  private Set<String> updatedProperties;

  /**
   * Flags indicating the dirty properties on the bean.
   */
  private boolean[] dirtyProperties;

  /**
   * Flag set when request is added to JDBC batch.
   */
  private boolean batched;

  /**
   * Flag set when batchOnCascade to avoid using batch on the top bean.
   */
  private boolean skipBatchForTopLevel;

  /**
   * Flag set when batch mode is turned on for a persist cascade.
   */
  private boolean batchOnCascadeSet;

  /**
   * Set for updates to determine if all loaded properties are included in the update.
   */
  private boolean requestUpdateAllLoadedProps;

  private long version;

  private long now;

  /**
   * Flag set when request is added to JDBC batch registered as a "getter callback" to automatically flush batch.
   */
  private boolean getterCallback;

  public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager<T> mgr, SpiTransaction t,
                            PersistExecute persistExecute, PersistRequest.Type type, boolean saveRecurse, boolean publish) {

    super(server, t, persistExecute);
    this.entityBean = (EntityBean) bean;
    this.intercept = entityBean._ebean_getIntercept();
    this.beanManager = mgr;
    this.beanDescriptor = mgr.getBeanDescriptor();
    this.beanPersistListener = beanDescriptor.getPersistListener();
    this.bean = bean;
    this.parentBean = parentBean;
    this.controller = beanDescriptor.getPersistController();
    this.type = type;
    this.docStoreMode = calcDocStoreMode(transaction, type);
    if (saveRecurse) {
      this.persistCascade = t.isPersistCascade();
    }

    if (this.type == Type.UPDATE) {
      if (intercept.isNew()) {
        // 'stateless update' - set loaded properties as dirty
        intercept.setNewBeanForUpdate();
        statelessUpdate = true;
      }
      // Mark Mutable scalar properties (like Hstore) as dirty where necessary
      beanDescriptor.checkMutableProperties(intercept);
    }
    this.concurrencyMode = beanDescriptor.getConcurrencyMode(intercept);
    this.publish = publish;
    if (isMarkDraftDirty(publish)) {
      beanDescriptor.setDraftDirty(entityBean, true);
    }
    this.dirty = intercept.isDirty();
  }

  /**
   * Return the document store event that should be used for this request.
   * <p>
   * Used to check if the Transaction has set the mode to IGNORE when doing large batch inserts that we
   * don't want to send to the doc store.
   */
  private DocStoreMode calcDocStoreMode(SpiTransaction txn, Type type) {
    DocStoreMode txnMode = (txn == null) ? null : txn.getDocStoreMode();
    return beanDescriptor.getDocStoreMode(type, txnMode);
  }

  /**
   * Return true if the draftDirty property should be set to true for this request.
   */
  private boolean isMarkDraftDirty(boolean publish) {
    return !publish && type != Type.DELETE && beanDescriptor.isDraftable();
  }

  /**
   * Set the transaction from prior persist request.
   * Only used when hard deleting draft & associated live beans.
   */
  public void setTrans(SpiTransaction transaction) {
    this.transaction = transaction;
    this.createdTransaction = false;
    this.persistCascade = transaction.isPersistCascade();
  }

  /**
   * Init the transaction and also check for batch on cascade escalation.
   */
  public void initTransIfRequiredWithBatchCascade() {

    if (createImplicitTransIfRequired()) {
      docStoreMode = calcDocStoreMode(transaction, type);
    }
    if (transaction.checkBatchEscalationOnCascade(this)) {
      // we escalated to use batch mode so flush when done
      // but if createdTransaction then commit will flush it
      batchOnCascadeSet = !createdTransaction;
    }
    persistCascade = transaction.isPersistCascade();
  }

  /**
   * If using batch on cascade flush if required.
   */
  public void flushBatchOnCascade() {
    if (batchOnCascadeSet) {
      // we escalated to batch mode for request so flush
      transaction.flushBatchOnCascade();
      batchOnCascadeSet = false;
    }
  }

  @Override
  public void rollbackTransIfRequired() {
    if (batchOnCascadeSet) {
      transaction.flushBatchOnRollback();
      batchOnCascadeSet = false;
    }
    super.rollbackTransIfRequired();
  }

  /**
   * Return true is this request was added to the JDBC batch.
   */
  public boolean isBatched() {
    return batched;
  }

  /**
   * Set when request is added to the JDBC batch.
   */
  public void setBatched() {
    batched = true;
    if (type == Type.INSERT || type == Type.UPDATE) {
      // used to trigger automatic jdbc batch flush
      intercept.registerGetterCallback(this);
      getterCallback = true;
    }
  }

  @Override
  public void preGetterTrigger() {
    transaction.flushBatch();
  }

  public void setSkipBatchForTopLevel() {
    skipBatchForTopLevel = true;
  }

  @Override
  public boolean isBatchThisRequest() {
    return !skipBatchForTopLevel && super.isBatchThisRequest();
  }

  /**
   * Return true if this is an insert request.
   */
  public boolean isInsert() {
    return Type.INSERT == type;
  }

  @Override
  public Set<String> getLoadedProperties() {
    return intercept.getLoadedPropertyNames();
  }

  @Override
  public Set<String> getUpdatedProperties() {
    return intercept.getDirtyPropertyNames();
  }

  /**
   * Return the dirty properties on this request.
   */
  public boolean[] getDirtyProperties() {
    return dirtyProperties;
  }

  /**
   * Return true if any of the given property names are dirty.
   */
  @Override
  public boolean hasDirtyProperty(Set<String> propertyNames) {
    return intercept.hasDirtyProperty(propertyNames);
  }

  /**
   * Return true if any of the given properties are dirty.
   */
  public boolean hasDirtyProperty(int[] propertyPositions) {

    for (int propertyPosition : propertyPositions) {
      if (dirtyProperties[propertyPosition]) {
        return true;
      }
    }
    return false;
  }

  @Override
  public Map<String, ValuePair> getUpdatedValues() {
    return intercept.getDirtyValues();
  }

  /**
   * Set the cache notify status.
   */
  private void setNotifyCache() {
    this.notifyCache = beanDescriptor.isCacheNotify(type, publish);
  }

  /**
   * Return true if this change should notify cache, listener or doc store.
   */
  public boolean isNotify() {
    return notifyCache || isNotifyPersistListener() || isDocStoreNotify();
  }

  /**
   * Return true if this request should update the document store.
   */
  private boolean isDocStoreNotify() {
    return docStoreMode != DocStoreMode.IGNORE;
  }

  private boolean isNotifyPersistListener() {
    return beanPersistListener != null;
  }

  /**
   * Collect L2 cache changes to be applied after the transaction has successfully committed.
   */
  public void notifyCache(CacheChangeSet changeSet) {
    if (notifyCache) {
      switch (type) {
        case INSERT:
          beanDescriptor.cacheHandleInsert(this, changeSet);
          break;
        case UPDATE:
          beanDescriptor.cacheHandleUpdate(idValue, this, changeSet);
          break;
        case DELETE:
        case SOFT_DELETE:
          beanDescriptor.cacheHandleDelete(idValue, this, changeSet);
          break;
        default:
          throw new IllegalStateException("Invalid type " + type);
      }
    }
  }

  /**
   * Process the persist request updating the document store.
   */
  @Override
  public void docStoreUpdate(DocStoreUpdateContext txn) throws IOException {

    switch (type) {
      case INSERT:
        beanDescriptor.docStoreInsert(idValue, this, txn);
        break;
      case UPDATE:
      case SOFT_DELETE:
        beanDescriptor.docStoreUpdate(idValue, this, txn);
        break;
      case DELETE:
        beanDescriptor.docStoreDeleteById(idValue, txn);
        break;
      default:
        throw new IllegalStateException("Invalid type " + type);
    }
  }

  /**
   * Add this event to the queue entries in IndexUpdates.
   */
  @Override
  public void addToQueue(DocStoreUpdates docStoreUpdates) {
    switch (type) {
      case INSERT:
        docStoreUpdates.queueIndex(beanDescriptor.getDocStoreQueueId(), idValue);
        break;
      case UPDATE:
      case SOFT_DELETE:
        docStoreUpdates.queueIndex(beanDescriptor.getDocStoreQueueId(), idValue);
        break;
      case DELETE:
        docStoreUpdates.queueDelete(beanDescriptor.getDocStoreQueueId(), idValue);
        break;
      default:
        throw new IllegalStateException("Invalid type " + type);
    }
  }

  public void addToPersistMap(BeanPersistIdMap beanPersistMap) {

    beanPersistMap.add(beanDescriptor, type, idValue);
  }

  public void notifyLocalPersistListener() {
    if (beanPersistListener != null) {
      switch (type) {
        case INSERT:
          beanPersistListener.inserted(bean);
          break;

        case UPDATE:
          beanPersistListener.updated(bean, updatedProperties);
          break;

        case DELETE:
          beanPersistListener.deleted(bean);
          break;

        case SOFT_DELETE:
          beanPersistListener.softDeleted(bean);
          break;

        default:
      }
    }
  }

  public boolean isParent(Object o) {
    return o == parentBean;
  }

  /**
   * Return true if this bean has been already been persisted (inserted or updated) in this
   * transaction.
   */
  public boolean isRegisteredBean() {
    return transaction.isRegisteredBean(bean);
  }

  public void unRegisterBean() {
    transaction.unregisterBean(bean);
  }

  /**
   * The hash used to register the bean with the transaction.
   * <p>
   * Takes into account the class type and id value.
   * </p>
   */
  private Integer getBeanHash() {
    if (beanHash == null) {
      Object id = beanDescriptor.getId(entityBean);
      int hc = 92821 * bean.getClass().getName().hashCode();
      if (id != null) {
        hc += id.hashCode();
      }
      beanHash = hc;
    }
    return beanHash;
  }

  public void registerDeleteBean() {
    Integer hash = getBeanHash();
    transaction.registerDeleteBean(hash);
  }

  public void unregisterDeleteBean() {
    Integer hash = getBeanHash();
    transaction.unregisterDeleteBean(hash);
  }

  public boolean isRegisteredForDeleteBean() {
    if (transaction == null) {
      return false;
    } else {
      Integer hash = getBeanHash();
      return transaction.isRegisteredDeleteBean(hash);
    }
  }

  /**
   * Return the BeanDescriptor for the associated bean.
   */
  public BeanDescriptor<T> getBeanDescriptor() {
    return beanDescriptor;
  }

  /**
   * Return true if a stateless update should also delete any missing details beans.
   */
  public boolean isDeleteMissingChildren() {
    return deleteMissingChildren;
  }

  /**
   * Set if deleteMissingChildren occurs on cascade save to OneToMany or ManyToMany.
   */
  public void setDeleteMissingChildren(boolean deleteMissingChildren) {
    this.deleteMissingChildren = deleteMissingChildren;
  }

  /**
   * Prepare the update after potential modifications in a BeanPersistController.
   */
  public void postControllerPrepareUpdate() {
    if (intercept.isNew() && controller != null) {
      // 'stateless update' - set dirty properties modified in controller preUpdate
      intercept.setNewBeanForUpdate();
    }
  }

  /**
   * Used to skip updates if we know the bean is not dirty. This is the case for EntityBeans that
   * have not been modified.
   */
  public boolean isDirty() {
    return dirty;
  }

  /**
   * Return the concurrency mode used for this persist.
   */
  public ConcurrencyMode getConcurrencyMode() {
    return concurrencyMode;
  }

  /**
   * Returns a description of the request. This is typically the bean class name or the base table
   * for MapBeans.
   * <p>
   * Used to determine common persist requests for queueing and statement batching.
   * </p>
   */
  public String getFullName() {
    return beanDescriptor.getFullName();
  }

  /**
   * Return the bean associated with this request.
   */
  @Override
  public T getBean() {
    return bean;
  }

  public EntityBean getEntityBean() {
    return entityBean;
  }

  /**
   * Return the Id value for the bean.
   */
  public Object getBeanId() {
    return beanDescriptor.getId(entityBean);
  }

  /**
   * Create and return a new reference bean matching this beans Id value.
   */
  public T createReference() {
    return beanDescriptor.createReference(Boolean.FALSE, false, getBeanId(), null);
  }

  /**
   * Return true if the bean type is a Draftable.
   */
  public boolean isDraftable() {
    return beanDescriptor.isDraftable();
  }

  /**
   * Return true if this request is a hard delete of a draftable bean.
   * If this is true Ebean is expected to auto-publish and delete the associated live bean.
   */
  public boolean isHardDeleteDraft() {
    if (type == Type.DELETE && beanDescriptor.isDraftable() && !beanDescriptor.isDraftableElement()) {
      // deleting a top level draftable bean
      if (beanDescriptor.isLiveInstance(entityBean)) {
        throw new PersistenceException("Explicit Delete is not allowed on a 'live' bean - only draft beans");
      }
      return true;
    }
    return false;
  }

  /**
   * Return true if this was a hard/permanent delete request (and should cascade as such).
   */
  public boolean isHardDeleteCascade() {
    return (type == Type.DELETE && beanDescriptor.isSoftDelete());
  }

  /**
   * Checks for @Draftable entity beans with @Draft property that the bean is a 'draft'.
   * Save or Update is not allowed to execute using 'live' beans - must use publish().
   */
  public void checkDraft() {
    if (beanDescriptor.isDraftable() && beanDescriptor.isLiveInstance(entityBean)) {
      throw new PersistenceException("Save or update is not allowed on a 'live' bean - only draft beans");
    }
  }

  /**
   * Return the parent bean for cascading save with unidirectional relationship.
   */
  public Object getParentBean() {
    return parentBean;
  }

  /**
   * Return the controller if there is one associated with this type of bean. This returns null if
   * there is no controller associated.
   */
  public BeanPersistController getBeanController() {
    return controller;
  }

  /**
   * Return the intercept if there is one.
   */
  public EntityBeanIntercept getEntityBeanIntercept() {
    return intercept;
  }

  /**
   * Return true if this property is loaded (full bean or included in partial bean).
   */
  public boolean isLoadedProperty(BeanProperty prop) {
    return intercept.isLoadedProperty(prop.getPropertyIndex());
  }

  @Override
  public int executeNow() {
    if (getterCallback) {
      intercept.clearGetterCallback();
    }
    switch (type) {
      case INSERT:
        executeInsert();
        return -1;

      case UPDATE:
        if (beanPersistListener != null) {
          // store the updated properties for sending later
          updatedProperties = getUpdatedProperties();
        }
        executeUpdate();
        return -1;

      case SOFT_DELETE:
        prepareForSoftDelete();
        executeSoftDelete();
        return -1;

      case DELETE:
        return executeDelete();

      default:
        throw new RuntimeException("Invalid type " + type);
    }
  }

  /**
   * Soft delete is executed as update so we want to set deleted=true property.
   */
  private void prepareForSoftDelete() {

    beanDescriptor.setSoftDeleteValue(entityBean);
  }

  @Override
  public int executeOrQueue() {

    boolean batch = isBatchThisRequest();
    try {
      BatchControl control = transaction.getBatchControl();
      if (control != null) {
        return control.executeOrQueue(this, batch);
      }
      if (batch) {
        control = persistExecute.createBatchControl(transaction);
        return control.executeOrQueue(this, true);

      } else {
        return executeNow();
      }
    } catch (BatchedSqlException e) {
      throw transaction.translate(e.getMessage(), e.getCause());
    }
  }

  /**
   * Set the generated key back to the bean. Only used for inserts with getGeneratedKeys.
   */
  @Override
  public void setGeneratedKey(Object idValue) {
    if (idValue != null) {
      // remember it for logging summary
      this.idValue = beanDescriptor.convertSetId(idValue, entityBean);
    }
  }

  /**
   * Set the Id value that was bound. Used for the purposes of logging summary information on this
   * request.
   */
  public void setBoundId(Object idValue) {
    this.idValue = idValue;
  }

  /**
   * Check for optimistic concurrency exception.
   */
  @Override
  public final void checkRowCount(int rowCount) {
    if (ConcurrencyMode.VERSION == concurrencyMode && rowCount != 1) {
      String m = Message.msg("persist.conc2", String.valueOf(rowCount));
      throw new OptimisticLockException(m, null, bean);
    }
    switch (type) {
      case DELETE:
      case SOFT_DELETE:
        postDelete();
        break;
      case UPDATE:
        postUpdate();
        break;
      default: // do nothing
    }
  }

  /**
   * Clear the bean from the PersistenceContext (L1 cache) for stateless updates.
   */
  private void postUpdate() {
    if (statelessUpdate) {
      beanDescriptor.contextClear(transaction.getPersistenceContext(), idValue);
    }
  }

  /**
   * Aggressive L1 and L2 cache cleanup for deletes.
   */
  private void postDelete() {
    beanDescriptor.contextClear(transaction.getPersistenceContext(), idValue);
  }

  private void changeLog() {
    BeanChange changeLogBean = beanDescriptor.getChangeLogBean(this);
    if (changeLogBean != null) {
      transaction.addBeanChange(changeLogBean);
    }
  }

  /**
   * Post processing.
   */
  @Override
  public void postExecute() {

    changeLog();

    if (controller != null) {
      controllerPost();
    }
    setNotifyCache();

    if (type == Type.UPDATE && (notifyCache || docStoreMode == DocStoreMode.UPDATE)) {
      // get the dirty properties for update notification to the doc store
      dirtyProperties = intercept.getDirtyProperties();
    }
    // if bean persisted again then should result in an update
    intercept.setLoaded();
    if (isInsert()) {
      postInsert();
    }

    addEvent();

    if (isLogSummary()) {
      logSummary();
    }
  }

  private void controllerPost() {
    switch (type) {
      case INSERT:
        controller.postInsert(this);
        break;
      case UPDATE:
        controller.postUpdate(this);
        break;
      case SOFT_DELETE:
        controller.postSoftDelete(this);
        break;
      case DELETE:
        controller.postDelete(this);
        break;
      default:
        break;
    }
  }

  private void logSummary() {

    String draft = (beanDescriptor.isDraftable() && !publish) ? " draft[true]" : "";
    String name = beanDescriptor.getName();
    switch (type) {
      case INSERT:
        transaction.logSummary("Inserted [" + name + "] [" + idValue + "]" + draft);
        break;
      case UPDATE:
        transaction.logSummary("Updated [" + name + "] [" + idValue + "]" + draft);
        break;
      case DELETE:
        transaction.logSummary("Deleted [" + name + "] [" + idValue + "]" + draft);
        break;
      case SOFT_DELETE:
        transaction.logSummary("SoftDelete [" + name + "] [" + idValue + "]" + draft);
        break;
      default:
        break;
    }
  }

  /**
   * Add the bean to the TransactionEvent. This will be used by TransactionManager to sync Cache,
   * Cluster and text indexes.
   */
  private void addEvent() {

    TransactionEvent event = transaction.getEvent();
    if (event != null) {
      event.add(this);
    }
  }

  /**
   * Determine the concurrency mode depending on fully/partially populated bean.
   * <p>
   * Specifically with version concurrency we want to check that the version property was one of the
   * loaded properties.
   * </p>
   */
  public ConcurrencyMode determineConcurrencyMode() {

    // 'partial bean' update/delete...
    if (concurrencyMode.equals(ConcurrencyMode.VERSION)) {
      // check the version property was loaded
      BeanProperty prop = beanDescriptor.getVersionProperty();
      if (prop == null || !intercept.isLoadedProperty(prop.getPropertyIndex())) {
        concurrencyMode = ConcurrencyMode.NONE;
      }
    }

    return concurrencyMode;
  }

  /**
   * Return true if the update DML/SQL must be dynamically generated.
   * <p>
   * This is the case for updates/deletes of partially populated beans.
   * </p>
   */
  public boolean isDynamicUpdateSql() {
    return beanDescriptor.isUpdateChangesOnly() || !intercept.isFullyLoadedBean();
  }

  /**
   * Return true if the property should be included in the update.
   */
  public boolean isAddToUpdate(BeanProperty prop) {
    if (requestUpdateAllLoadedProps) {
      return intercept.isLoadedProperty(prop.getPropertyIndex());
    } else {
      return intercept.isDirtyProperty(prop.getPropertyIndex());
    }
  }

  /**
   * Register the derived relationships to get executed later (on JDBC batch flush or commit).
   */
  public void deferredRelationship(EntityBean assocBean, ImportedId importedId, EntityBean bean) {
    transaction.registerDeferred(new PersistDeferredRelationship(ebeanServer, beanDescriptor, assocBean, importedId, bean));
  }

  private void postInsert() {
    // mark all properties as loaded after an insert to support immediate update
    int len = intercept.getPropertyLength();
    for (int i = 0; i < len; i++) {
      intercept.setLoadedProperty(i);
    }
    beanDescriptor.setEmbeddedOwner(entityBean);
    if (!publish) {
      beanDescriptor.setDraft(entityBean);
    }
  }

  public boolean isReference() {
    return beanDescriptor.isReference(intercept);
  }

  /**
   * This many property has been cascade saved. Keep note of this and update the 'many property'
   * cache on post commit.
   */
  public void addUpdatedManyProperty(BeanPropertyAssocMany<?> updatedAssocMany) {
    if (updatedManys == null) {
      updatedManys = new ArrayList<>(5);
    }
    updatedManys.add(updatedAssocMany);
  }

  /**
   * Return the list of cascade updated many properties (can be null).
   */
  public List<BeanPropertyAssocMany<?>> getUpdatedManyCollections() {
    return updatedManys;
  }

  /**
   * Check if any of its many properties where cascade saved and hence we need to update related
   * many property caches.
   */
  public void checkUpdatedManysOnly() {
    if (!dirty && updatedManys != null) {
      // set the flag and register for post commit processing if there
      // is caching or registered listeners
      if (idValue == null) {
        this.idValue = beanDescriptor.getId(entityBean);
      }
      updatedManysOnly = true;
      setNotifyCache();
      addEvent();
    }
  }

  /**
   * Return true if only many properties where updated.
   */
  public boolean isUpdatedManysOnly() {
    return updatedManysOnly;
  }

  /**
   * For requests that update document store add this event to either the list
   * of queue events or list of update events.
   */
  public void addDocStoreUpdates(DocStoreUpdates docStoreUpdates) {

    if (type == Type.UPDATE) {
      beanDescriptor.docStoreUpdateEmbedded(this, docStoreUpdates);
    }
    switch (docStoreMode) {
      case UPDATE: {
        docStoreUpdates.addPersist(this);
        return;
      }
      case QUEUE: {
        if (type == Type.DELETE) {
          docStoreUpdates.queueDelete(beanDescriptor.getDocStoreQueueId(), idValue);
        } else {
          docStoreUpdates.queueIndex(beanDescriptor.getDocStoreQueueId(), idValue);
        }
      }
      default:
        break;
    }
  }

  /**
   * Determine if all loaded properties should be used for an update.
   * <p>
   * Takes into account transaction setting and JDBC batch.
   * </p>
   */
  private boolean determineUpdateAllLoadedProperties() {

    Boolean txnUpdateAll = transaction.isUpdateAllLoadedProperties();
    if (txnUpdateAll != null) {
      // use the setting explicitly set on the transaction
      requestUpdateAllLoadedProps = txnUpdateAll;
    } else {
      // if using batch use the server default setting
      requestUpdateAllLoadedProps = isBatchThisRequest() && ebeanServer.isUpdateAllPropertiesInBatch();
    }

    return requestUpdateAllLoadedProps;
  }

  /**
   * Return true if this request is a 'publish' action.
   */
  public boolean isPublish() {
    return publish;
  }

  /**
   * Return the key for an update persist request.
   */
  public String getUpdatePlanHash() {

    StringBuilder key;
    if (determineUpdateAllLoadedProperties()) {
      key = intercept.getLoadedPropertyKey();
    } else {
      key = intercept.getDirtyPropertyKey();
    }

    BeanProperty versionProperty = beanDescriptor.getVersionProperty();
    if (versionProperty != null) {
      if (intercept.isLoadedProperty(versionProperty.getPropertyIndex())) {
        key.append('v');
      }
    }

    if (publish) {
      key.append('p');
    }

    return key.toString();
  }

  /**
   * Return the table to update depending if the request is a 'publish' one or normal.
   */
  public String getUpdateTable() {
    return publish ? beanDescriptor.getBaseTable() : beanDescriptor.getDraftTable();
  }

  /**
   * Return true if this is a soft delete request.
   */
  public boolean isSoftDelete() {
    return Type.SOFT_DELETE == type;
  }

  /**
   * Set the value of the Version property on the bean.
   */
  public void setVersionValue(Object versionValue) {
    version = beanDescriptor.setVersion(entityBean, versionValue);
  }

  /**
   * Return the version in long form (if set).
   */
  public long getVersion() {
    return version;
  }

  public void executeInsert() {
    Object tenantId = transaction.getTenantId();
    if (tenantId != null) {
      beanDescriptor.setTenantId(entityBean, tenantId);
    }
    if (controller == null || controller.preInsert(this)) {
      beanManager.getBeanPersister().insert(this);
    }
  }

  public void executeUpdate() {
    if (controller == null || controller.preUpdate(this)) {
      postControllerPrepareUpdate();
      beanManager.getBeanPersister().update(this);
    }
  }

  public void executeSoftDelete() {
    if (controller == null || controller.preSoftDelete(this)) {
      postControllerPrepareUpdate();
      beanManager.getBeanPersister().update(this);
    }
  }

  public int executeDelete() {
    if (controller == null || controller.preDelete(this)) {
      return beanManager.getBeanPersister().delete(this);
    }
    // delete handled by the BeanController so return 0
    return 0;
  }

  /**
   * Persist to the document store now (via buffer, not post commit).
   */
  public void docStorePersist() {
    idValue = beanDescriptor.getId(entityBean);
    switch (type) {
      case UPDATE:
        dirtyProperties = intercept.getDirtyProperties();
        break;
    }
    // processing now so set IGNORE (unlike DB + DocStore processing with post-commit)
    docStoreMode = DocStoreMode.IGNORE;
    try {
      docStoreUpdate(transaction.getDocStoreTransaction().obtain());
      postExecute();
      if (type == Type.UPDATE
        && beanDescriptor.isDocStoreEmbeddedInvalidation()
        && transaction.isPersistCascade()) {
        // queue embedded/nested updates for later processing
        beanDescriptor.docStoreUpdateEmbedded(this, transaction.getDocStoreTransaction().queue());
      }
    } catch (IOException e) {
      throw new PersistenceException("Error persisting doc store bean", e);
    }
  }

  /**
   * Use a common 'now' value across both when created and when updated etc.
   */
  public long now() {
    if (now == 0) {
      now = System.currentTimeMillis();
    }
    return now;
  }

}
